41. 数据结构之元组

⭐ 元组(Tuple):Python中的”只读”数据结构

  • 元组是Python中一种基础且重要的数据结构
  • 核心特征:不可变性(Immutability)
  • 一旦创建,其元素和顺序就固定不变
  • 通过限制操作来确保数据完整性程序安全性

⭐ 为什么需要不可变的数据结构?

  • 线程安全:多线程环境下无需加锁即可安全访问
  • 哈希能力:只有不可变对象才能作为字典的键
  • 内存优化:Python解释器可以对元组进行特定优化
  • 语义清晰:不可变性明确传达了”只读”的编程意图

⭐ 元组在金融数据分析中的价值

  • 交易参数保护:止损线、杠杆比例等参数不应被意外修改
  • 数据完整性保证:历史价格、公司基本信息等静态数据
  • 多值返回:函数返回均值和标准差等多个相关值
  • 字典键的需求:按(日期, 股票代码)组合查找数据

⭐ 元组的五大核心特性

特性 说明
不可变性 创建后内容不能增加、删除或修改
有序性 元素按插入顺序存储,可通过索引访问
异构性 可包含不同类型的元素(整数、字符串等)
轻量性 比列表更节省内存空间
可迭代性 可用于for循环、推导式等

⭐ 异构性示例:一条股票Tick数据

tick_data = ('600519.SH', '2024-01-15 09:30:00', 1856.00, 100)
# 分别代表:股票代码、时间戳、价格、成交量
  • 一个元组可以包含不同类型的元素
  • 特别适合表示”记录”或”结构体”
  • 元素的位置赋予了元素语义

⭐ 创建元组——方法1:使用圆括号

Listing 1
# 方式1: 使用圆括号创建元组
# 圆括号是可选的,但加上可以提高代码可读性
tup1 = ('physics', 'chemistry', 1997, 2000)  # 包含字符串和整数的混合元组
tup2 = (1, 2, 3, 4, 5)  # 纯整数元组
tup3 = "a", "b", "c", "d"  # 不加括号也可以,Python会自动识别为元组

# 创建空元组
# 空元组常用于初始化变量或作为函数默认参数
empty_tup = ()

# 创建单元素元组(注意逗号!)
# 这是一个常见的陷阱:单个元素后面必须有逗号,否则会被识别为带括号的表达式
single_tup = (50,)  # 正确:有逗号,是元组
not_a_tuple = (50)  # 错误:没有逗号,这只是整数50

# 打印结果以验证类型
print('元组1:', tup1)
print('元组2:', tup2)
print('元组3:', tup3)  # 证明不加括号也能创建元组
print('空元组:', empty_tup)
print('单元素元组:', single_tup)
元组1: ('physics', 'chemistry', 1997, 2000)
元组2: (1, 2, 3, 4, 5)
元组3: ('a', 'b', 'c', 'd')
空元组: ()
单元素元组: (50,)

⭐ 注意:单元素元组的”逗号陷阱”

  • (50,) → 这是一个元组(有逗号)
  • (50) → 这只是整数50(没有逗号)
  • 单元素后面的逗号不能省略
single_tup = (50,)   # ✅ 正确:元组
not_a_tuple = (50)    # ❌ 错误:只是整数50

⭐ 创建元组——方法2:tuple()构造函数

Listing 2
# 从列表创建元组
list_data = [1, 2, 3, 4]
tup_from_list = tuple(list_data)

# 从字符串创建元组(每个字符成为一个元素)
tup_from_string = tuple('Hello')

# 从range对象创建元组
tup_from_range = tuple(range(5))

print('从列表:', tup_from_list)
print('从字符串:', tup_from_string)
print('从range:', tup_from_range)
从列表: (1, 2, 3, 4)
从字符串: ('H', 'e', 'l', 'l', 'o')
从range: (0, 1, 2, 3, 4)

⭐ 创建元组——方法3:生成器表达式

Listing 3
# 使用生成器表达式创建元组
# 生成器表达式用圆括号表示,然后传给tuple()函数
squares = tuple(x**2 for x in range(6))

# 这等价于先创建列表再转换,但更节省内存
# 因为生成器是惰性求值的,不需要创建中间列表

print('平方数元组:', squares)
平方数元组: (0, 1, 4, 9, 16, 25)

⭐ 创建方法选择指南

场景 推荐方法 理由
已知元素,数量少 tup = (1, 2) 简洁直观
从其他序列转换 tuple(seq) 清晰表达意图
需要计算生成元素 tuple(x for x in ...) 内存高效
需要空元组 empty = () 最简洁

⭐ 元组的索引:正向与负向

Listing 4
# 创建示例元组
tup1 = ('physics', 'chemistry', 1997, 2000)  # 混合类型元组
tup2 = (1, 2, 3, 4, 5, 6, 7)  # 纯数字元组

# 正向索引:从0开始,从左向右数
print('tup1[0]:', tup1[0])  # 第一个元素:'physics'
print('tup1[1]:', tup1[1])  # 第二个元素:'chemistry'

# 负向索引:从-1开始,从右向左数
print('tup2[-1]:', tup2[-1])  # 最后一个元素:7
print('tup2[-2]:', tup2[-2])  # 倒数第二个元素:6

# 切片操作:获取子元组
# 语法:tup[start:end:step]
# start包含,end不包含(左闭右开区间)
print('tup2[1:5]:', tup2[1:5])  # 索引1到4的元素:(2, 3, 4, 5)
print('tup2[::2]:', tup2[::2])  # 每隔一个取一个:(1, 3, 5, 7)
print('tup2[::-1]:', tup2[::-1])  # 反转元组:(7, 6, 5, 4, 3, 2, 1)
tup1[0]: physics
tup1[1]: chemistry
tup2[-1]: 7
tup2[-2]: 6
tup2[1:5]: (2, 3, 4, 5)
tup2[::2]: (1, 3, 5, 7)
tup2[::-1]: (7, 6, 5, 4, 3, 2, 1)

⭐ 切片操作详解

语法 含义 示例
[start:end] 从start到end-1 tup[1:5]
[start:end:step] 按步长取元素 tup[::2]
[:end] 从头开始 tup[:3]
[start:] 到结尾 tup[2:]
[::-1] 反转序列 tup[::-1]

⭐ 索引越界会报错

Listing 5
tup = (1, 2, 3)

# 下面的代码会抛出IndexError
try:
    print(tup[10])  # 索引10不存在
except IndexError as e:
    print(f'错误:索引超出范围 - {e}')
错误:索引超出范围 - tuple index out of range
  • 元组长度固定,访问不存在的索引会引发 IndexError
  • 可使用 try/except 或先检查 len() 来避免

⭐ 不可变性的含义

  • ❌ 不能修改元组中的元素
  • ❌ 不能删除元组中的元素
  • ❌ 不能添加新元素到元组
  • ✅ 可以重新赋值变量为新的元组
  • ✅ 可以连接两个元组创建新元组

⭐ 不可变性演示:修改会报错

Listing 6
# 创建一个元组
tup = (12, 34.56)

# 尝试修改元组的第一个元素
try:
    tup[0] = 20  # 这会抛出TypeError
except TypeError as e:
    print(f'错误类型: {type(e).__name__}')
    print(f'错误信息: {e}')
    print('结论:元组元素一旦创建就不能修改')

# 虽然不能修改,但可以连接两个元组创建新元组
# 注意:这不是修改原元组,而是创建了一个全新的元组
tup3 = tup + ('abc', 'xyz')
print('原元组:', tup)  # 原元组保持不变
print('新元组:', tup3)  # 这是一个新创建的元组

# 也可以通过重复创建新元组
tup4 = tup * 2  # 元组重复
print('重复元组:', tup4)
错误类型: TypeError
错误信息: 'tuple' object does not support item assignment
结论:元组元素一旦创建就不能修改
原元组: (12, 34.56)
新元组: (12, 34.56, 'abc', 'xyz')
重复元组: (12, 34.56, 12, 34.56)

⭐ 为什么需要不可变性?

理由 说明
数据安全性 历史交易记录一旦生成就不应被修改
多线程环境 不可变对象是线程安全的,无需加锁
哈希要求 只有不可变对象才能作为字典键
函数式编程 使程序更易于推理和测试
内存优化 Python可对相同内容的元组共享内存

⭐ 应用1:交易参数配置

Listing 7
# 定义交易策略的核心参数
# 使用元组确保这些参数不会被意外修改
trading_params = (
    100000,    # 初始资金(元)
    0.05,       # 预期年收益率
    30,         # 投资期限(年)
    0.6         # 股票配置比例
)

# 元组解包:将元组的元素分别赋值给变量
# 这种写法比分别访问元组元素更清晰
initial_capital, rate, years, stock_ratio = trading_params

# 格式化输出参数
# 使用f-string进行格式化,提高可读性
print(f'初始资金: {initial_capital:,.0f}元')  # 添加千位分隔符
print(f'年利率: {rate:.1%}')  # 百分比格式
print(f'投资期限: {years}年')
print(f'股票配置: {stock_ratio:.0%}')

# 计算期末资产
final_amount = initial_capital * (1 + rate) ** years
print(f'期末资产(复利): {final_amount:,.2f}元')
初始资金: 100,000元
年利率: 5.0%
投资期限: 30年
股票配置: 60%
期末资产(复利): 432,194.24元

⭐ 应用2:元组作为字典键

Listing 8
# 创建价格查找表
# 键是(股票代码, 日期)的元组,值是收盘价
price_lookup = {
    ('600519.SH', '2024-01-15'): 1856.00,
    ('600519.SH', '2024-01-16'): 1862.50,
    ('000858.SZ', '2024-01-15'): 158.20,
    ('000858.SZ', '2024-01-16'): 159.80
}

# 查询特定股票在特定日期的价格
stock = '600519.SH'
date = '2024-01-15'
price = price_lookup.get((stock, date))

if price:
    print(f'{stock}{date} 的收盘价是 {price:.2f}元')
else:
    print('未找到匹配的价格数据')

# 只有不可变对象(如元组)才能作为字典键
# 下面的代码会报错,因为列表是可变的:
# error_dict = {['600519.SH', '2024-01-15']: 1856.00}  # TypeError
600519.SH 在 2024-01-15 的收盘价是 1856.00元

⭐ 应用3:函数返回多个值

Listing 9
def calculate_return_metrics(prices):
    '''
    计算价格序列的多个收益率指标

    参数:
        prices: 价格列表

    返回:
        (总收益率, 年化收益率, 波动率)的元组
    '''
    import numpy as np

    # 计算简单收益率
    returns = np.diff(prices) / prices[:-1]

    # 总收益率
    total_return = (prices[-1] - prices[0]) / prices[0]

    # 年化收益率(假设252个交易日)
    annual_return = (1 + total_return) ** (252 / len(prices)) - 1

    # 年化波动率
    annual_volatility = returns.std() * np.sqrt(252)

    return total_return, annual_return, annual_volatility

# 使用示例
stock_prices = [100, 102, 101, 105, 108, 107, 110]
total_ret, ann_ret, ann_vol = calculate_return_metrics(stock_prices)

print(f'总收益率: {total_ret:.2%}')
print(f'年化收益率: {ann_ret:.2%}')
print(f'年化波动率: {ann_vol:.2%}')
总收益率: 10.00%
年化收益率: 2991.27%
年化波动率: 30.26%

⭐ 应用4:时间序列数据记录

Listing 10
# 定义一个时间戳数据点
tick = ('600519.SH', '2024-01-15 09:30:00', 1856.00, 100)
# 格式:(股票代码, 时间戳, 价格, 成交量)

# 元组解包提取字段
symbol, timestamp, price, volume = tick

print(f'股票代码: {symbol}')
print(f'时间戳: {timestamp}')
print(f'价格: {price:.2f}元')
print(f'成交量: {volume}手')

# 将多个tick数据组成列表
ticks = [
    ('600519.SH', '09:30:00', 1856.00, 100),
    ('600519.SH', '09:31:00', 1858.50, 150),
    ('600519.SH', '09:32:00', 1855.00, 80),
]

# 分析第一分钟的价格变化
price_change = ticks[1][2] - ticks[0][2]
print(f'第一分钟价格变化: {price_change:+.2f}元')
股票代码: 600519.SH
时间戳: 2024-01-15 09:30:00
价格: 1856.00元
成交量: 100手
第一分钟价格变化: +2.50元

⭐ 元组 vs 列表:如何选择?

选择元组 选择列表
数据需要保护、不应修改 数据会频繁增删
需要用作字典键 逐步构建数据序列
性能敏感,大量小对象 需要临时存储中间结果
函数返回多个值 需要排序、追加等操作
固定的记录结构 动态变化的集合

⭐ 性能对比:元组更轻更快

Listing 11
import sys
import timeit

# 创建相同内容的元组和列表
tup = tuple(range(1000))
lst = list(range(1000))

# 内存占用比较
print(f'元组内存占用: {sys.getsizeof(tup)} 字节')
print(f'列表内存占用: {sys.getsizeof(lst)} 字节')
print(f'元组更节省: {sys.getsizeof(lst) - sys.getsizeof(tup)} 字节')

# 访问速度比较
tup_time = timeit.timeit('x = tup[500]', globals=globals(), number=1000000)
lst_time = timeit.timeit('x = lst[500]', globals=globals(), number=1000000)

print(f'元组访问时间: {tup_time:.4f}秒')
print(f'列表访问时间: {lst_time:.4f}秒')
print(f'元组速度提升: {(lst_time/tup_time - 1)*100:.1f}%')
元组内存占用: 8040 字节
列表内存占用: 8056 字节
元组更节省: 16 字节
元组访问时间: 0.0270秒
列表访问时间: 0.0249秒
元组速度提升: -7.8%

⭐ 命名元组:带字段名的增强版元组

Listing 12
from collections import namedtuple

# 定义一个命名元组类型
# 相当于创建了一个简单的类
Stock = namedtuple('Stock', ['symbol', 'price', 'volume'])

# 创建Stock实例
stock1 = Stock('600519.SH', 1856.00, 1000)
stock2 = Stock(symbol='000858.SZ', price=158.20, volume=5000)

# 可以像元组一样索引访问
print(f'股票代码(索引): {stock1[0]}')

# 也可以像对象一样属性访问
print(f'股票代码(属性): {stock1.symbol}')
print(f'价格: {stock1.price}')
print(f'成交量: {stock1.volume}')

# 命名元组仍然是元组,是不可变的
print(f'是否为元组: {isinstance(stock1, tuple)}')

# 命名元组有友好的字符串表示
print(f'股票信息: {stock1}')
股票代码(索引): 600519.SH
股票代码(属性): 600519.SH
价格: 1856.0
成交量: 1000
是否为元组: True
股票信息: Stock(symbol='600519.SH', price=1856.0, volume=1000)

⭐ 总结:元组核心要点

  • 不可变性是元组的灵魂——确保数据安全与一致性
  • 根据场景选择数据结构——不变用元组,变化用列表
  • 善用元组解包——使代码更清晰、更Pythonic
  • 注意嵌套可变对象——元组内的列表仍可被修改
  • 性能优势——内存更省、访问更快

⭐ 平台任务1解答代码

Listing 13
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
name = "中国通号"  # 设置名称为"中国通号"
print('name:',type(name))  # 输出name
 
code = "688009"  # 设置代码为"688009"
print('code:',type(code))  # 输出code
 
IPO_date = "2019年7月22日"  # 设置日期为"2019年7月22日"
print('IPO_data:',type(IPO_date))  # 输出IPO_data
 
exchange = "上海证券交易所"  #以字符串输入中国通号A股的上市交易所
print('exchange',type(exchange))               #使用type函数输出数据类型
 
capital = 10589819000  # 总股本:10589819000股
print('capital',type(capital))  # 输出capital
 
shares = 8621018000  # 流通股本:8621018000股
print('share:',type(shares))  # 输出share
 
price_IPO = 5.85          #以浮点型输入中国通号A股发行价
print('price_IPO:',type(price_IPO))    #使用type函数输出数据类型并使用print函数打印
 
price_Jul22 = 12.27  # 设置当前价格为12.27
print('price_Jul22:',type(price_Jul22))  # 输出price_Jul22
 
change_Jul22 = 1.09744  # 涨幅:109.744%
print('change_Jul22:',type(change_Jul22))  # 输出change_Jul22
name: <class 'str'>
code: <class 'str'>
IPO_data: <class 'str'>
exchange <class 'str'>
capital <class 'int'>
share: <class 'int'>
price_IPO: <class 'float'>
price_Jul22: <class 'float'>
change_Jul22: <class 'float'>

⭐ 平台任务2解答代码

Listing 14
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
name = "中国通号"  # 设置名称为"中国通号"
code = "688009"  # 设置代码为"688009"
IPO_date = "2019年7月22日"  # 设置日期为"2019年7月22日"
exchange = "上海证券交易所"  # 设置交易所为"上海证券交易所"
capital = 10589819000  # 总股本:10589819000股
shares = 8621018000  # 流通股本:8621018000股
price_IPO = 5.85  # 设置当前价格为5.85
price_Jul22 = 12.27  # 设置当前价格为12.27
change_Jul22 = 1.09744  # 涨幅:109.744%
 
tup =(name,code,IPO_date,exchange,capital,shares,price_IPO,price_Jul22,change_Jul22)                  #创建一个元组,包括以上数据
print(tup)                                     #打印输出这个元组
    
print(tup[0])                                             #访问该元组的首个元素
print(tup[-1])                                             #访问该元组的末尾元素
print(tup[2:6])#访问该元组的第3个至第6个元素
('中国通号', '688009', '2019年7月22日', '上海证券交易所', 10589819000, 8621018000, 5.85, 12.27, 1.09744)
中国通号
1.09744
('2019年7月22日', '上海证券交易所', 10589819000, 8621018000)

⭐ 平台任务3解答代码

Listing 15
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
name = "中国通号"  # 设置名称为"中国通号"
code = "688009"  # 设置代码为"688009"
IPO_date = "2019年7月22日"  # 设置日期为"2019年7月22日"
exchange = "上海证券交易所"  # 设置交易所为"上海证券交易所"
capital = 10589819000  # 总股本:10589819000股
shares = 8621018000  # 流通股本:8621018000股
price_IPO = 5.85  # 设置当前价格为5.85
price_Jul22 = 12.27  # 设置当前价格为12.27
change_Jul22 = 1.09744  # 涨幅:109.744%
tup = (name,code,IPO_date,exchange,capital,shares,price_IPO,price_Jul22,change_Jul22)  # 定义元组tup
 
price_Jul22_H = 5.43     #以浮点型输入中国通号H股7月22日收盘价
change_Jul22_H = -0.1171 #以浮点数输入中国通号H股7月22日涨跌幅
del tup                  #删除任务2中创建的元组
tup_new = (name,code,IPO_date,exchange,capital,shares,price_IPO,price_Jul22,change_Jul22,price_Jul22_H,change_Jul22_H)   #创建包含H股股价和涨跌幅的新元组
print(tup_new)  #打印这个新元组
('中国通号', '688009', '2019年7月22日', '上海证券交易所', 10589819000, 8621018000, 5.85, 12.27, 1.09744, 5.43, -0.1171)